<?php
declare (strict_types = 1);

namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;

class CrontabMysqlBackup extends Command
{
    protected $pid_file;
    protected $log_file;
    protected $after_timer;
    protected $tick_timer;
    protected $period;
    protected $worktime;
    protected $bin_dir;
    protected $bak_dir;
    protected $keep;

    protected function configure()
    {
        // 指令配置
        $this->setName('crontab:mysqlbackup')
            ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|status|backup", 'start')
            ->setDescription('数据库备份服务');

        $this->log_file = runtime_path() . 'log' . DIRECTORY_SEPARATOR . 'mysqlbackup.log';
        // pid文件不要放到runtime目录，否则容易被清除掉，造成进程无法正常关闭
        $this->pid_file = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'marks' . DIRECTORY_SEPARATOR . 'mysqlbackup.pid';

        // 运行周期（秒）
        $this->period = 86400;
        // 运行时间点，进程生命周期内只有一个值，不一定就是上一次/下一次的运行时间点，运行周期在时间轴上分割出一系列的点，这些点平移后，其中的某一个点与运行时间点重合
        $this->worktime = strtotime(date('Y-m-d 03:00:00'));
        // mysqldump命令所在目录
        $this->bin_dir = '/usr/bin/';
        // 备份文件存放目录
        $this->bak_dir = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR;
        // 备份文件保留时间（秒）
        $this->keep = 7 * 86400;
    }

    protected function execute(Input $input, Output $output)
    {
        if (!extension_loaded('swoole')) {
            $output->error('Can\'t detect Swoole extension installed.');
            return;
        }
        $action = $input->getArgument('action');
        if (in_array($action, ['start', 'stop', 'restart', 'status', 'backup'])) {
            $this->$action();
        } else {
            $output->error("Invalid argument action:{$action}, Expected start|stop|restart|status|backup.");
            return;
        }
    }

    protected function checkFile($file)
    {
        $dir = dirname($file);
        if (!is_dir($dir)) {
            @mkdir($dir, 0700, true);
        }
        @touch($file);
        if (!is_writable($file)) {
            return false;
        }
        return true;
    }

    protected function backup()
    {
        $database = config('database.connections.mysql.database');
        $username = config('database.connections.mysql.username');
        $password = config('database.connections.mysql.password');

        $host = config('database.connections.mysql.hostname');
        $port = config('database.connections.mysql.hostport');
        $charset = config('database.connections.mysql.charset');

        $time = date('Ymd_His');

        $filename = "{$this->bak_dir}{$database}_{$time}.gz";
        if (!$this->checkFile($filename)) {
            echo "[" . date('Y-m-d H:i:s') . "]\r\nCan\'t create file under: {$this->bak_dir}.\r\n\r\n";
            return;
        }
        @unlink($filename);

        $this->clear($database);

        $command = "{$this->bin_dir}mysqldump -h{$host} -P{$port} --default-character-set={$charset} -u{$username} -p'{$password}' {$database}|gzip>{$filename}";
        $descriptorspec = array(
            0 => array("pipe", "r"),
            1 => array("pipe", "w"),
            2 => array("pipe", "w"),
        );
        $env = array('option_key' => 'option_value');
        $process = proc_open($command, $descriptorspec, $pipes, null, $env);
        if (is_resource($process)) {

            //fwrite($pipes[0], "{$password}\n"); //密码不走标准输入，如需自动输入密码，可参考expect相关命令
            fclose($pipes[0]);

            $stdout = stream_get_contents($pipes[1]);
            fclose($pipes[1]);
            $stderr = stream_get_contents($pipes[2]);
            fclose($pipes[2]);
            $return_value = proc_close($process);

            if (!empty($err = $this->getError($return_value, $stderr))) {
                echo "[" . date('Y-m-d H:i:s') . "]\r\nBackup failure: {$err}\r\n\r\n";
                return;
            }
        }
    }

    protected function getError($return_value, $stderr)
    {
        if (!empty($return_value)) {
            return "Command returned $return_value";
        }
        $err = explode("\n", str_replace("\r\n", "\n", trim($stderr)));
        if ($err[0] != 'mysqldump: [Warning] Using a password on the command line interface can be insecure.') {
            return $stderr;
        }
        if (count($err) > 1) {
            array_shift($err);
            return join("\n", $err);
        }
        return '';
    }

    protected function clear($filePrefix)
    {
        foreach (glob($this->bak_dir . '*.gz') as $file) {
            if (preg_match('/^' . $filePrefix . '_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.gz$/', basename($file), $matches)) {
                $time = mktime((int) $matches[4], (int) $matches[5], (int) $matches[6], (int) $matches[2], (int) $matches[3], (int) $matches[1]);
                if ($time !== false && $time <= time() - $this->keep) {
                    @unlink($file);
                }
            }
        }
    }

    protected function start()
    {
        if ($this->isRunning()) {
            $this->output->error('Process is already running.');
            return;
        }
        $this->output->writeln('Starting process...');

        if (!$this->checkFile($this->pid_file)) {
            $this->output->error("Can\'t write file: {$this->pid_file}.");
            return;
        }
        if (!$this->checkFile($this->log_file)) {
            $this->output->error("Can\'t write file: {$this->log_file}.");
            return;
        }

        $timespan = ($this->worktime - time()) % $this->period;
        if ($timespan <= 0) {
            $timespan += $this->period;
        }

        $this->after_timer = \swoole_timer::after($timespan * 1000, function () {
            $this->tick_timer = \swoole_timer::tick($this->period * 1000, function () {
                ob_start();
                $this->backup();
                file_put_contents($this->log_file, ob_get_clean(), FILE_APPEND);
            });
            ob_start();
            $this->backup();
            file_put_contents($this->log_file, ob_get_clean(), FILE_APPEND);
        });

        \swoole_process::daemon(true, false);

        file_put_contents($this->pid_file, getmypid());

        \swoole_process::signal(SIGTERM, function () {
            if ($this->after_timer) {
                \swoole_timer::clear($this->after_timer);
            }
            if ($this->tick_timer) {
                \swoole_timer::clear($this->tick_timer);
            }
            @unlink($this->pid_file);
        });

        $nextTime = date('Y-m-d H:i:s', time() + $timespan);
        $this->output->writeln("Starting success, Task will run in the {$nextTime} for the first time.");

        \swoole_event::wait();
    }

    protected function stop()
    {
        if (!$this->isRunning()) {
            $this->output->error('No process running.');
            return;
        }
        $this->output->writeln('Stopping process...');
        $pid = (int) file_get_contents($this->pid_file);
        \swoole_process::kill($pid, SIGTERM);
        $end = time() + 15;
        while (time() < $end && \swoole_process::kill($pid, 0)) {
            usleep(100000);
        }
        if (\swoole_process::kill($pid, 0)) {
            $this->output->error('Unable to stop the process.');
            return;
        }
        $this->output->writeln('Stopping success.');
    }

    protected function restart()
    {
        if ($this->isRunning()) {
            $this->stop();
        }
        $this->start();
    }

    protected function status()
    {
        $this->output->writeln($this->isRunning() ? 'Process is running.' : 'Process stopped.');
    }

    protected function isRunning()
    {
        if (!is_readable($this->pid_file)) {
            return false;
        }
        $pid = (int) file_get_contents($this->pid_file);
        return $pid > 0 && \swoole_process::kill($pid, 0);
    }
}
